<?php

namespace Core\Common\Base\Db;

use Core\Common\Base\Config;
use Core\Common\Route\Request;

class Database
{
    const DB_NAME = 'main';

    protected static $connMap = [];
    protected static $data_fields = [];
    protected static $partitions = null;
    protected static $cache = [];

    /**
     * 获取数据库连接配置
     * @param boolean $master
     * @param string $dbname
     * @return mixed
     */
    public static function getConf($master = false, $dbname = '')
    {
        $dbConfig = Config::config('database.' . $dbname);
        if (empty($dbConfig)) {
            return false;
        }
        if (isset($dbConfig['host'])) {
            return $dbConfig;
        }

        if ($master && isset($dbConfig['write'])) {
            $setting = $dbConfig['write'];
        } elseif (!$master && isset($dbConfig['read'])) {
            $setting = $dbConfig['read'];
        }
        if (!isset($setting)) {
            return false;
        }

        $setting['host'] = explode(',', $setting['host']);
        return $setting;
    }

    public static function init()
    {
        if (!is_null(self::$partitions)) {
            return;
        }
        self::$partitions = Config::getTables();
        foreach (self::$partitions as $t => $info) {
            self::$partitions[$t]['num'] = array_sum($info['database']);
        }
    }

    /**
     * 返回数据库连接配置
     * @param bool $master
     * @param string $dbName
     * @return array
     */
    public static function getConnectionConfig($master = false, $dbName = '')
    {
        if (empty($dbName)) {
            $dbName = static::DB_NAME;
        }

        if (!isset(self::$connMap[$dbName][$master])) {
            $conf = self::getConf($master, $dbName);
            self::$connMap[$dbName][$master] = $conf;
        } else {
            $conf = self::$connMap[$dbName][$master];
        }
        return $conf;
    }

    /**
     * 获取连接
     * @param bool $master
     * @param string $dbName
     * @param null $driver
     * @return bool|mixed|null
     */
    public static function getConnection($master = false, $dbName = '', $driver = null)
    {
        if (Request::isRunningInMars()) {
            $dbName = 'mars_mock';
        }
        $conf = self::getConnectionConfig($master, $dbName);
        if (empty($conf)) {
            return false;
        }
        $db = Connection::getDBConnection($conf, $dbName, $driver);
        self::$connMap[$dbName][$master] = $conf;
        return $db;
    }

    /**
     * 单表返回表名(字符串), 多表多库返回 $ret[serial] = tableName;
     * @param type $arr where条件数组 或 需要insert的数据
     * @return array
     */
    public static function getTableName($arr = [], $table = '')
    {
        self::init();
        if (empty($table)) {
            $table = static::TABLE_NAME;
        }
        //没有多表配置, 认为只有一张表
        if (!isset(self::$partitions[$table])) {
            return [$table];
        }
        //有多表配置, 且只有一张表
        $conf = self::$partitions[$table];
        if (self::$partitions[$table]['num'] == 1) {
            return [$table];
        }
        return self::parse($arr, $conf, $table);
    }

    public static function parse($arr, $conf, $table = '')
    {
        if (empty($table)) {
            $table = static::TABLE_NAME;
        }

        if (isset($conf['type']) == 'date') {
            $format = '_%s';
        } else {
            //增加配置项，兼容老的分表
            if (isset($conf['format']) && !$conf['format']) {
                $format = '_%d';
            } else {
                $format = '_%03d';
            }
        }

        //有主键
        if (isset($conf['primary']) && isset($arr[$conf['primary']])) {
            if (!is_array($arr[$conf['primary']])) {
                $serial = self::getSerialNum($conf, $arr[$conf['primary']]);
                return [$serial => $table . sprintf($format, $serial)];
            }
            $tmp = [];
            foreach ($arr[$conf['primary']] as $val) {
                $serial = self::getSerialNum($conf, $val);
                if (!isset($tmp[$serial])) {
                    $tmp[$serial] = $table . sprintf($format, $serial);
                }
            }
            return $tmp;
        }
        foreach ($arr as $key => $value) {
            if (is_int($key) && is_array($value)) {
                $ret = [];
                foreach ($arr as $v) {
                    $ret += self::parse($v, $conf);
                }
                return $ret;
            } elseif (is_int($key) && !is_array($value)) {//需要解析的where条件
                $i = 0;
                $tmp = [];
                while ($i < $conf['num']) {
                    $serial = self::getSerialNum($conf, $i);
                    $tmp[$serial] = $table . sprintf($format, $serial);
                    ++$i;
                }
                return $tmp;
            }
        }
        if (empty($arr)) {
            $i = 0;
            $tmp = [];
            while ($i < $conf['num']) {
                $serial = self::getSerialNum($conf, $i);
                $tmp[$serial] = $table . sprintf($format, $serial);
                ++$i;
            }
            return $tmp;
        }
        return [1 => $table . sprintf($format, 1)];
    }

    /**
     * 获取多表时的序号
     * @param array $conf
     * @param int $val
     * @return string
     */
    public static function getSerialNum($conf, $val)
    {
        if ($conf['type'] == 'mod') {
            $mod = bcmod($val, $conf['num']);
            return $mod;
        }
        if ($conf['type'] == 'mod_crc32') {
            $mod = bcmod(crc32($val), $conf['num']);
            return $mod;
        }
        if ($conf['type'] == 'range') {
            $mod = intval($val / $conf['step']) + 1;
            return $mod;
        }
        if ($conf['type'] == 'date') {
            if ($conf['date'] == 'month') {
                return date('Ym');
            }
            return date('Y');
        }
        return '';
    }

    /**
     * 获取数据表所有的库名, 因为有可能数据表分布在不同的库里
     * @param type $serial
     * @return string
     */
    public static function getDbName($serial = 0)
    {
        self::init();
        //没有多表配置, 认为只有一张表
        if (!isset(self::$partitions[static::TABLE_NAME])) {
            return static::DB_NAME;
        }
        if (!isset(self::$partitions[static::TABLE_NAME]['database'])) {
            return static::DB_NAME;
        }
        //使用连接池
        if (!empty(self::$partitions[static::TABLE_NAME]['usePool'])) {
            return self::$partitions[static::TABLE_NAME]['usePool'];
        }
        $dbs = self::$partitions[static::TABLE_NAME]['database'];
        if (count($dbs) == 1) {
            return key($dbs);
        }
        $index = 0;
        foreach ($dbs as $dbName => $num) {
            $index += $num;
            if ($serial <= $index) {
                return $dbName;
            }
        }
        return '';
    }

    public static function checkMulti($tableSetting)
    {
        $ret = [];
        foreach ($tableSetting as $serial => $_v) {
            $dbName = self::getDbName($serial);
            $ret[$dbName][] = $serial;
        }
        return $ret;
    }

    public static function reloadFields()
    {
        self::$data_fields = [];
    }

    public static function fields()
    {
        if (isset(self::$data_fields[static::TABLE_NAME])) {
            return self::$data_fields[static::TABLE_NAME];
        }
        $tableSetting = self::getTableName();
        $dbName = self::getDbName(key($tableSetting));
        $table = current($tableSetting);
        $db = self::getConnection(false, $dbName);
        $desc = $db->describe($table);
        self::$data_fields[static::TABLE_NAME] = array_column($desc, 'Field');
        return self::$data_fields[static::TABLE_NAME];
    }

    public static function dataType()
    {
        $tableSetting = self::getTableName();
        $dbName = self::getDbName(key($tableSetting));
        $table = current($tableSetting);
        $db = self::getConnection(false, $dbName);
        $desc = $db->describe($table);
        $arr = array_column($desc, 'Type');
        $ret = [];
        foreach ($arr as $type) {
            if ($type != 'point' && (false !== strpos($type, 'int'))) {
                $ret[] = 'int';
            } elseif ($type != 'point' && (false !== strpos($type, 'float'))) {
                $ret[] = 'double';
            } else {
                $ret[] = 'string';
            }
        }
        return $ret;
    }

    public static function tables()
    {
        $tableSetting = self::getTableName();
        $dbName = self::getDbName(key($tableSetting));
        $db = self::getConnection(false, $dbName);
        return $db->tables();
    }

    public static function desc($table)
    {
        $tableSetting = self::getTableName();
        $dbName = self::getDbName(key($tableSetting));
        $db = self::getConnection(false, $dbName);
        return $db->describe($table);
    }

    /**
     * 同库跨表查询
     * @param type $lines
     */
    public static function multiGet($lines)
    {
        foreach ($lines as $line) {
            list($table, $where, $option, $fields) = $line;
            $tableSetting = self::getTableName($where);
        }
    }

    /**
     * 同一逻辑表中的查询
     * 支持多表在同一库中时批量查询
     * @param type $where
     * @param type $option
     * @param type $fields
     * @return type
     */
    public static function getData($fields = [], $where = [], $option = [])
    {
        $tableSetting = self::getTableName($where);
        $ret = [];
        $count = count($tableSetting);
        $multi = self::checkMulti($tableSetting);
        $option['multi'] = true;
        $id = isset($option['id']) ? $option['id'] : '';
        $overwrite = isset($option['overwrite']) ? $option['overwrite'] : null;
        $value = isset($option['value']) ? $option['value'] : null;
        //多表,无查询条件,需要限制查询数量
        if ($count > 1 && empty($where) && !isset($option['limit'])) {
            $option['limit'] = intval(2000 / $count);
        }
        foreach ($multi as $dbName => $serials) {
            $sqls = [];
            $db = self::getConnection(false, $dbName);
            foreach ($serials as $serial) {
                $tableName = $tableSetting[$serial];
                $sqls[] = $db->select($tableName, $where, $option, $fields);
            }
            if ($sqls) {
                $tmp = $db->multiQuery($sqls, $id, $overwrite, $value);
                if ($count == 1) {
                    return $tmp;
                }
                if (isset($tmp[0])) {
                    $ret = array_merge($ret, $tmp);
                } else {
                    $ret += $tmp;
                }
            }
        }
        return $ret;
    }

    public static function union($whereArr = [], $option = [], $fields = [])
    {
        $ret = [];
        $option['multi'] = true;
        $id = isset($option['id']) ? $option['id'] : '';
        $overwrite = isset($option['overwrite']) ? $option['overwrite'] : null;
        $value = isset($option['value']) ? $option['value'] : null;
        $sqls = [];
        foreach ($whereArr as $where) {
            $tableSetting = self::getTableName($where);
            $multi = self::checkMulti($tableSetting);
            foreach ($multi as $dbName => $serials) {
                $db = self::getConnection(false, $dbName);
                foreach ($serials as $serial) {
                    $tableName = $tableSetting[$serial];
                    $sqls[$dbName][] = '(' . $db->select($tableName, $where, $option, $fields) . ')';
                }
            }
        }
        foreach ($sqls as $dbName => $ss) {
            $db = self::getConnection(false, $dbName);
            $tmp = $db->query(implode(' UNION ', $ss), $id, $overwrite, $value);
            if (isset($tmp[0])) {
                $ret = array_merge($ret, $tmp);
            } else {
                $ret += $tmp;
            }
        }
        return $ret;
    }

    public static function getOne($fields = [], $where = [], $option = [])
    {
        $option['limit'] = 1;
        $arRet = self::getData($fields, $where, $option);
        return !empty($arRet) ? $arRet[0] : [];
    }

    public static function pluck($where, $key)
    {
        $arRet = self::getOne($where, [], [$key]);
        return isset($arRet[$key]) ? $arRet[$key] : null;
    }

    /**
     * 根据条件返回Key-Value数组, key值相同会被覆盖
     * @param array $where
     * @param mixed $k
     * @param mixed $v
     * @param array $ops
     * @return array
     */
    public static function getKv($where, $k, $v, $ops = [])
    {
        $fields = array_merge((array)$k, (array)$v);
        $option = ['id' => $k, 'value' => $v, 'overwrite' => true];
        foreach ($ops as $op => $ov) {
            $option[$op] = $ov;
        }
        $arRet = self::getData(array_unique($fields), $where, $option);
        return !empty($arRet) ? $arRet : [];
    }

    /**
     * 根据条件返回Key-Value数组, 不会覆盖key值相同的值, Value是数组
     * @param array $where
     * @param mixed $k
     * @param mixed $v
     * @param array $ops
     * @return array
     */
    public static function getMap($where, $k, $v, $ops = [])
    {
        $fields = array_merge((array)$k, (array)$v);
        $option = ['id' => $k, 'value' => $v];
        foreach ($ops as $op => $ov) {
            $option[$op] = $ov;
        }
        $arRet = self::getData($where, $option, array_unique($fields));
        return !empty($arRet) ? $arRet : [];
    }

    public static function updateData($vals, $where, $rawKey = [])
    {
        if (empty($where)) {
            return true;
        }
        $tableSetting = self::getTableName($where);
        $ret = 0;
        foreach ($tableSetting as $serial => $tableName) {
            $dbName = self::getDbName($serial);
            $db = self::getConnection(true, $dbName);
            $db->update($tableName, $vals, $where, $rawKey);
            $ret += $db->affectedRow();
        }
        return $ret;
    }

    public static function addUploadData($arr, $fields, $rawkey = [])
    {
        $db = self::getConnection(true);
        $db->replace(static::TABLE_NAME, $arr, $rawkey, $fields);
        return $db->affectedRow();
    }

    /**
     * 设置下一个自增ID
     * @param type $id
     */
    public static function setNextId($id)
    {
        $db = self::getConnection(true);
        $db->setNextId(static::TABLE_NAME, $id);
    }

    /**
     * 多表时按各表拆分需要插入的数据
     * @param type $arr
     * @return type
     */
    private static function splitInsertData($arr)
    {
        if (!isset($arr[0])) {
            $arr = [$arr];
        }
        self::init();
        //没有多表配置, 认为只有一张表
        if (!isset(self::$partitions[static::TABLE_NAME])) {
            return [0 => $arr];
        }
        if (!isset(self::$partitions[static::TABLE_NAME]['num'])) {
            return [0 => $arr];
        }
        $conf = self::$partitions[static::TABLE_NAME];

        $ret = [];
        foreach ($arr as $line) {
            $serial = self::getSerialNum($conf, $line[$conf['primary']]);
            $ret[$serial][] = $line;
        }
        return $ret;
    }

    public static function addData($arr, $returnId = true)
    {
        $tableSetting = self::getTableName($arr);
        $data = self::splitInsertData($arr);
        $ret = [];
        $count = count($tableSetting);
        foreach ($tableSetting as $serial => $tableName) {
            $dbName = self::getDbName($serial);
            $db = self::getConnection(true, $dbName);
            $db->insert($tableName, $data[$serial]);
            if ($returnId) {
                $ret[] = $db->insertId();
                continue;
            }
            $ret[] = $db->affectedRow();
        }
        if ($count == 1) {
            return array_pop($ret);
        }
        return $ret;
    }

    public static function addDuplicateData($arr, $update, $rawkey = [], $returnId = true)
    {
        $tableSetting = self::getTableName($arr);
        $data = self::splitInsertData($arr);
        $ret = [];
        $count = count($tableSetting);
        foreach ($tableSetting as $serial => $tableName) {
            $dbName = self::getDbName($serial);
            $db = self::getConnection(true, $dbName);
            $db->insert($tableName, $data[$serial], $update, $rawkey);
            if ($returnId) {
                $ret[] = $db->insertId();
                continue;
            }
            $ret[] = $db->affectedRow();
        }
        if ($count == 1) {
            return array_pop($ret);
        }
        return $ret;
    }

    public static function deleteData($where)
    {
        if (empty($where)) {
            return true;
        }
        $tableSetting = self::getTableName($where);
        $ret = 0;
        foreach ($tableSetting as $serial => $tableName) {
            $dbName = self::getDbName($serial);
            $db = self::getConnection(true, $dbName);
            $db->delete($tableName, $where);
            $ret += $db->affectedRow();
        }
        return $ret;
    }

    public static function begin($dbName)
    {
        $db = self::getConnection(true, $dbName);
        if (!defined('CORE_MYSQL_DRIVER') || CORE_MYSQL_DRIVER == 'mysqli') {
            $db->autocommit(false);
        } else {
            $db->beginTransaction();
        }
    }

    public static function commit($dbName)
    {
        $db = self::getConnection(true, $dbName);
        $db->commit();
        if (!defined('CORE_MYSQL_DRIVER') || CORE_MYSQL_DRIVER == 'mysqli') {
            $db->autocommit(true);
        }
    }

    public static function rollback($dbName)
    {
        $db = self::getConnection(true, $dbName);
        if (!defined('CORE_MYSQL_DRIVER') || CORE_MYSQL_DRIVER == 'mysqli') {
            $db->rollback();
            $db->autocommit(true);
        } else {
            $db->rollBack();
        }
    }

    /**
     *
     * @param type $database
     * @param type $table
     * @return \self
     */
    public static function getStaticInstance($database, $table)
    {
        if (!isset(self::$cache[$database][$table])) {
            $classname = null;
            $baseClass = __CLASS__;
            //过滤非法字符
            if (preg_replace('#\w+#', '', $database) || preg_replace('#\w+#', '', $table)) {
                return null;
            }
            eval("\$classname = new class extends $baseClass {
                const DB_NAME = '$database';
                const TABLE_NAME = '$table';
            };");
            self::$cache[$database][$table] = $classname;
        }
        return self::$cache[$database][$table];
    }
}
